<?php
namespace Tlf\Tester;
class CurlBrowser {
/**
* The default host to connect to, such as http://localhost:3183
*/
public string $default_host;
/**
* Array of responses as built by `curl_get_response()`, in the order they were received
*/
public array $responses = [];
public function __construct(?string $default_host = null){
$this->default_host = $default_host;
}
/**
* the body of the last response
*/
public function __toString(){
return end($this->responses)['body'];
}
/**
* @return the last response or false if no responses
*/
public function last(){
return end($this->responses);
}
/**
* Follow the redirect sent with the last request
* @return true if a redirect happened, false if there was no `Location` header to goto
*/
public function follow(){
$response = end($this->responses);
if (!isset($response['headers']['Location']))return false;
$this->get($response['headers']['Location']);
return true;
}
public function get($path, $params=[], $headers=[], $curl_opts = []){
$ch = curl_init();
$url = $this->make_url($path,$params);
if (isset($headers['Cookie'])){
$cookie = $headers['Cookie'];
unset($headers['Cookie']);
curl_setopt($ch, CURLOPT_COOKIE, $cookie);
}
$curl_headers = [];
foreach ($headers as $key=>$value){
if (is_int($key)){
$curl_headers[] = $value;
continue;
}
$curl_headers[] = $key.': '.$value;
}
curl_setopt_array($ch,
[
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => false,
CURLOPT_HEADER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_HTTPHEADER => $curl_headers,
]
);
if (count($curl_opts) > 0 ){
curl_setopt_array($ch, $curl_opts);
}
$response = $this->curl_get_response($ch);
curl_close($ch);
$this->responses[] = $response;
return $response;
}
/**
*
* @beta
*
* @param $params POST params
* @param $headers array like `['HeaderKey: header_value', 'Cookie'=> 'cookie_key=cookie_value;cookie2=value2;']` ... ONLY `Cookie` should have an array key. All other header keys should be in the array value and use a numeric index
* @param $files ... idr
* @param $curl_opts array of php `CURLOPT`s to set
*/
public function post($path, $params=[], $headers=[], $files=[], $curl_opts = []){
$ch = curl_init();
$url = $this->make_url($path,[]);
if (isset($headers['Cookie'])){
$cookie = $headers['Cookie'];
unset($headers['Cookie']);
curl_setopt($ch, CURLOPT_COOKIE, $cookie);
}
$curl_headers = [];
foreach ($headers as $key=>$value){
if (is_int($key)){
$curl_headers[] = $value;
continue;
}
$curl_headers[] = $key.': '.$value;
}
foreach ($files as $key=>$f){
if (!is_file($f)){
throw new \Exception("File '$f' for key '$key' does not exist or is not a file.");
}
$cf = new \CURLFile($f, null, basename($f));
$params[$key] = $cf;
}
if (is_string($params)){
$post_fields = $params;
}else if (count($files) > 0){
$post_fields = $params;
} else {
$post_fields = http_build_query($params);
}
curl_setopt_array($ch,
[
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $post_fields,
CURLOPT_HEADER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_HTTPHEADER => $curl_headers,
]
);
if (count($curl_opts) > 0 ){
foreach ($curl_opts as $key=>$value){
curl_setopt($ch, $key, $value);
}
// curl_setopt_array($ch, $curl_opts);
}
$response = $this->curl_get_response($ch);
curl_close($ch);
$this->responses[] = $response;
return $response;
}
/**
* @param $url the url to connect to. if default_host is set, just needs a path
* @param $params array of paramaters to encode into the url. If your url path includes `?params=whatever`, it will not play nicely with this.
*/
public function make_url(string $url, array $params=[]): string {
$parsed = parse_url($url);
if (!isset($parsed[PHP_URL_HOST]))$url = $this->default_host.$url;
if ($params!=[]) $url .= '?' . http_build_query($params);
return $url;
}
/**
* @param $header_value, for `Set-cookie: whatever whatever`, this should be `whatever whatever`
* @return array parsed cookie
*
*
* @issue has no handling for a cookie value containing a semi-colon or an equal sign
*/
public function parse_cookie_header($header_value): array {
$cookie = [];
$parts = explode(';', $header_value);
$parts = array_map('trim',$parts);
$main = array_shift($parts);
$main_parts = explode('=', $main);
$cookie['name'] = $main_parts[0];
$cookie['value'] = $main_parts[1];
foreach ($parts as $str){
$kv_parts = explode('=', $str);
if (count($kv_parts)==1)$cookie[$str] = true;
else $cookie[$kv_parts[0]] = $kv_parts[1];
}
return $cookie;
}
/**
* @param $ch curl handle
* @param $files array of key=>absolute file paths
* @param $params array to use as CURLOPT_POSTFIELDS
*/
public function curl_add_files($ch, array $files, array &$params){
foreach ($files as $name=>$path){
$mimetype = mime_content_type($path);
// $mimetype = false;
if ($mimetype==false){
$ext = pathinfo($path,PATHINFO_EXTENSION);
$list = require(dirname(__DIR__).'/mime_type_map.php');
$mimetype = $list['mimes'][$ext][0];
}
$params[$name] = new \CURLFile($path, $mimetype, basename($path));
}
}
public function curl_get_response($ch){
$response = curl_exec($ch);
$header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
$header_text = substr($response,0,$header_size);
$parts = explode("\n", $header_text);
$parts = array_map('trim', $parts);
$headers = [];
$cookies = [];
foreach ($parts as $line){
$pos = strpos($line,':');
if ($pos===false)continue;
$key = substr($line,0,$pos);
$value = trim(substr($line,$pos+1));
if (isset($headers[$key])){
if (!is_array($headers[$key]))$headers[$key] = [$headers[$key]];
$headers[$key][] = $value;
} else {
$headers[$key] = $value;
}
if ($key=='Set-Cookie'){
$cookie = $this->parse_cookie_header($value);
$cookies[$cookie['name']] = $cookie;
}
}
// TODO: parse Set-Cookie headers
$body = substr($response,$header_size);
return [
'header_text'=>$header_text,
'body'=>$body,
'headers'=>$headers,
'cookies'=>$cookies,
];
}
}